package com.studerb.tasks;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.SwingWorker;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;

import com.studerb.events.TaskEvent;
import com.studerb.nntp.GroupCommandReply;
import com.studerb.nntp.NntpConnection;
import com.studerb.service.NetworkMonitorService;
import com.studerb.service.interfaces.IConnectionPoolService;
import com.studerb.tasks.interfaces.ITask;
import com.studerb.tasks.interfaces.ITaskListener;

/**
 * A type of {@link SwingWorker} that represents an application background task.
 * Tasks add descriptive properties that can be shown to the user, a new set of
 * methods for customizing task completion, and a {@code TaskListener} that
 * enables one to monitor the three key SwingWorker methods: {@code
 * doInBackground}, {@code process} and {@code done}.
 * <p>
 * When a Task completes, the {@code final done} method invokes one of {@code
 * succeeded}, {@code cancelled}, {@code interrupted}, or {@code failed}. The
 * {@code final done} method invokes {@code finished} when the completion method
 * returns or throws an exception.
 * <p>
 * For example: given a Task called {@code MyTask} defined like this:
 * 
 * <pre>
 * class MyTask extends Task&lt;MyResultType, Void&gt; {
 * 	protected MyResultType doInBackground() {
 * 		message(&quot;startMessage&quot;, getPlannedSubtaskCount());
 * 		// do the work ... if an error is encountered:
 * 		message(&quot;errorMessage&quot;);
 * 		message(&quot;finishedMessage&quot;, getActualSubtaskCount(), getFailureCount());
 * 		// .. return the result
 * 	}
 * }
 * </pre>
 * 
 * Typically the resources for this class would be defined in the MyTask
 * ResourceBundle, @{code resources/MyTask.properties}:
 * 
 * <pre>
 * title = My Task 
 * description = A task of mine for my own purposes.   
 * startMessage = Starting: working on %s subtasks...  
 * errorMessage = An unexpected error occurred, skipping subtask
 * finishedMessage = Finished: completed %1$s subtasks, %2$s failures
 * </pre>
 * <p>
 * Task subclasses can override resource values in their own ResourceBundles:
 * 
 * <pre>
 * class MyTaskSubclass extends MyTask {
 * }
 * # resources/MyTaskSubclass.properties
 * title = My Task Subclass 
 * description = An appropriate description
 * # ... all other resources are inherited
 * </pre>
 * <p>
 * All of the settable properties in this class are bound, i.e. a
 * PropertyChangeEvent is fired when the value of the property changes. As with
 * the {@code SwingWorker} superclass, all {@code PropertyChangeListeners} run
 * on the event dispatching thread. This is also true of {@code TaskListeners}.
 * <p>
 * Unless specified otherwise specified, this class is thread-safe. All of the
 * Task properties can be get/set on any thread.
 */
public abstract class AbstractTask<T, V> extends SwingWorker<T, V> implements ITask<T> {

	public final static int MAX_ERRORS = 5;

	protected static final Logger logger = Logger.getLogger(AbstractTask.class);
	protected static AtomicInteger taskTitle = new AtomicInteger();
	protected List<ITaskListener<T, V>> taskListeners;
	protected String title;
	protected String description = null;
	protected long messageTime = -1L;
	protected String message = null;
	protected long startTime = -1L;
	protected long doneTime = -1L;
	protected boolean userCanCancel = true;
	protected boolean progressPropertyIsValid = false;
	protected boolean connectionNeeded = true;
	protected TaskPriority priority = TaskPriority.AVERAGE;
	protected NntpConnection nntpConnection;
	protected boolean determinable = true;
	protected int errorCount = 0;
	protected boolean failed = false;
	protected GroupCommandReply groupCommandReply;
	@Autowired NetworkMonitorService networkMonitorService;

	@Autowired IConnectionPoolService singelServerConnectionPoolService;

	public AbstractTask() {
		super();
		addPropertyChangeListener(new StatePCL());
		taskListeners = new CopyOnWriteArrayList<ITaskListener<T, V>>();
		// this.title = "Task" + (String.valueOf(taskTitle.getAndIncrement()));
	}

	/**
	 * Adds a {@code TaskListener} to this Task. The listener will be notified
	 * when the Task's state changes to {@code STARTED}, each time the {@code
	 * process} method is called, and when the Task's state changes to {@code
	 * DONE}. All of the listener methods will run on the event dispatching
	 * thread.
	 * 
	 * @param listener
	 *            the {@code TaskListener} to be added
	 * @see #removeTaskListener
	 */
	@Override public void addTaskListener(ITaskListener listener) {
		if (listener == null) {
			throw new IllegalArgumentException("null listener");
		}
		taskListeners.add(listener);
	}

	/**
	 * Called when this Task has been cancelled by {@link #cancel(boolean)}.
	 * <p>
	 * This method runs on the EDT. It does nothing by default.
	 * 
	 * @see #done
	 */
	protected void cancelled() {
		logger.info("Task: " + getTitle() + " cancelled");
	}

	@Override protected final void done() {
		try {
			if (isCancelled()) {
				cancelled();
			}
			else {
				try {
					succeeded(get());
				}
				catch (InterruptedException e) {
					interrupted(e);
				}
				catch (ExecutionException e) {
					failed(e.getCause());
				}
			}
		}
		finally {
			try {
				finished();
			}
			finally {
				// setTaskService(null);
			}
		}
	}

	/**
	 * Called when an execution of this Task fails and an {@code
	 * ExecutionExecption} is thrown by {@code get}.
	 * <p>
	 * This method runs on the EDT. It Logs an error message by default.
	 * 
	 * @param cause
	 *            the {@link Throwable#getCause cause} of the {@code
	 *            ExecutionException}
	 * @see #done
	 * @see #get
	 * @see #failed
	 */
	protected void failed(Throwable cause) {
		String msg = String.format("%s failed: %s", this, ExceptionUtils.getRootCauseMessage(cause));
		setMessage(msg);
		logger.error(msg);
		cause.printStackTrace();
	}

	/**
	 * Called unconditionally (in a {@code finally} clause) after one of the
	 * completion methods, {@code succeeded}, {@code failed}, {@code cancelled},
	 * or {@code interrupted}, runs. Subclasses can override this method to
	 * cleanup before the {@code done} method returns.
	 * <p>
	 * This method runs on the EDT. It does nothing by default.
	 * 
	 * @see #done
	 * @see #get
	 * @see #failed
	 */
	protected void finished() {
		logger.debug("Task: " + getTitle() + " finished");
	}

	/*
	 * This method runs on the EDT because it's called from StatePCL (see
	 * below).
	 */
	private void fireCancelledListeners() {
		TaskEvent<Void> event = new TaskEvent(this, null);
		for (ITaskListener<T, V> listener : taskListeners) {
			listener.cancelled(event);
		}
	}

	/*
	 * This method runs on the EDT because it's called from StatePCL (see
	 * below).
	 */
	private void fireCompletionListeners() {
		try {
			if (isCancelled()) {
				fireCancelledListeners();
			}
			else {
				try {
					fireSucceededListeners(get());
				}
				catch (InterruptedException e) {
					fireInterruptedListeners(e);
				}
				catch (ExecutionException e) {
					fireFailedListeners(e.getCause());
				}
			}
		}
		finally {
			fireFinishedListeners();
		}
	}

	/*
	 * This method runs on the EDT because it's called from StatePCL (see
	 * below).
	 */
	private void fireDoInBackgroundListeners() {
		TaskEvent<Void> event = new TaskEvent(this, null);
		for (ITaskListener<T, V> listener : taskListeners) {
			listener.doInBackground(event);
		}
	}

	/*
	 * This method runs on the EDT because it's called from StatePCL (see
	 * below).
	 */
	private void fireFailedListeners(Throwable e) {
		TaskEvent<Throwable> event = new TaskEvent(this, e);
		for (ITaskListener<T, V> listener : taskListeners) {
			listener.failed(event);
		}
	}

	/*
	 * This method runs on the EDT because it's called from StatePCL (see
	 * below).
	 */
	private void fireFinishedListeners() {
		TaskEvent<Void> event = new TaskEvent(this, null);
		for (ITaskListener<T, V> listener : taskListeners) {
			listener.finished(event);
		}
	}

	/*
	 * This method runs on the EDT because it's called from StatePCL (see
	 * below).
	 */
	private void fireInterruptedListeners(InterruptedException e) {
		TaskEvent<InterruptedException> event = new TaskEvent(this, e);
		for (ITaskListener<T, V> listener : taskListeners) {
			listener.interrupted(event);
		}
	}

	/*
	 * This method is guaranteed to run on the EDT, it's called from
	 * SwingWorker.process().
	 */
	private void fireProcessListeners(List<V> values) {
		TaskEvent<List<V>> event = new TaskEvent(this, values);
		for (ITaskListener<T, V> listener : taskListeners) {
			listener.process(event);
		}
	}

	/*
	 * This method runs on the EDT because it's called from StatePCL (see
	 * below).
	 */
	private void fireSucceededListeners(T result) {
		TaskEvent<T> event = new TaskEvent(this, result);
		for (ITaskListener<T, V> listener : taskListeners) {
			listener.succeeded(event);
		}
	}

	@Override public NntpConnection getConnectionion() {
		return nntpConnection;
	}

	public String getCurrentState() {
		StateValue stateValue = this.getState();
		if (stateValue == StateValue.DONE) {
			return "Done";
		}
		else if (stateValue == StateValue.PENDING) {
			return "Pending";
		}
		else if (stateValue == StateValue.STARTED) {
			return "Working";
		}
		else {
			return "Unknown";
		}
	}

	@Override public String getDescription() {
		return description;
	}

	/**
	 * Returns the length of time this Task has run. If the task hasn't started
	 * yet (i.e. if its state is still {@code StateValue.PENDING}), then this
	 * method returns 0. Otherwise it returns the duration in the specified time
	 * units. For example, to learn how many seconds a Task has run so far:
	 * 
	 * <pre>
	 * long nSeconds = myTask.getExecutionDuration(TimeUnit.SECONDS);
	 * </pre>
	 * 
	 * @param unit
	 *            the time unit of the return value
	 * @return the length of time this Task has run.
	 * @see #execute
	 */
	public long getExecutionDuration(TimeUnit unit) {
		long startTime, doneTime, dt;
		synchronized (this) {
			startTime = this.startTime;
			doneTime = this.doneTime;
		}
		if (startTime == -1L) {
			dt = 0L;
		}
		else if (doneTime == -1L) {
			dt = System.currentTimeMillis() - startTime;
		}
		else {
			dt = doneTime - startTime;
		}
		return unit.convert(Math.max(0L, dt), TimeUnit.MILLISECONDS);
	}

	@Override public boolean getFailed() {
		return failed;
	}

	public String getMessage() {
		return message;
	}

	/**
	 * Returns the length of time that has elapsed since the {@code message}
	 * property was last set.
	 * 
	 * @param unit
	 *            units for the return value
	 * @return elapsed time since the {@code message} property was last set.
	 * @see #setMessage
	 */
	public long getMessageDuration(TimeUnit unit) {
		long messageTime;
		synchronized (this) {
			messageTime = this.messageTime;
		}
		long dt = messageTime == -1L ? 0L : Math.max(0L, System.currentTimeMillis() - messageTime);
		return unit.convert(dt, TimeUnit.MILLISECONDS);
	}

	@Override public TaskPriority getPriority() {
		return priority;
	}

	/**
	 * Returns a copy of this Task's {@code TaskListeners}.
	 * 
	 * @return a copy of this Task's {@code TaskListeners}.
	 * @see #addTaskListener
	 * @see #removeTaskListener
	 */
	public ITaskListener<T, V>[] getTaskListeners() {
		return taskListeners.toArray(new ITaskListener[taskListeners.size()]);
	}

	@Override public String getTitle() {
		return title;
	}

	/**
	 * Returns the value of the {@code userCanCancel} property. The default
	 * value of this property is true.
	 * <p>
	 * Generic GUI components, like a Task progress dialog, can use this
	 * property to decide if they should provide a way for the user to cancel
	 * this task.
	 * 
	 * @return true if the user can cancel this Task.
	 * @see #setUserCanCancel
	 */
	public synchronized boolean getUserCanCancel() {
		return userCanCancel;
	}

	/**
	 * Called if the Task's Thread is interrupted but not explicitly cancelled.
	 * <p>
	 * This method runs on the EDT. It does nothing by default.
	 * 
	 * @param e
	 *            the {@code InterruptedException} thrown by {@code get}
	 * @see #cancel
	 * @see #done
	 * @see #get
	 */
	protected void interrupted(InterruptedException e) {
		logger.info("Task: " + getTitle() + " interrupted");
	}

	public boolean isConnectionNeeded() {
		return connectionNeeded;
	}

	@Override public boolean isDeterminable() {
		return this.isDeterminable();
	}

	/**
	 * Equivalent to {@code getState() == StateValue.PENDING}.
	 * <p>
	 * When a pending Task's state changes to {@code StateValue.STARTED} a
	 * PropertyChangeEvent for the "started" property is fired. Similarly when a
	 * started Task's state changes to {@code StateValue.DONE}, a "done"
	 * PropertyChangeEvent is fired.
	 */
	public final boolean isPending() {
		return getState() == StateValue.PENDING;
	}

	/**
	 * Returns true if the {@link #setProgress progress} property has been set.
	 * Some Tasks don't update the progress property because it's difficult or
	 * impossible to determine how what percentage of the task has been
	 * completed. GUI elements that display Task progress, like an application
	 * status bar, can use this property to set the @{link
	 * JProgressBar#indeterminate indeterminate} @{code JProgressBar} property.
	 * <p>
	 * A task that does keep the progress property up to date should initialize
	 * it to 0, to ensure that {@code isProgressPropertyValid} is always true.
	 * 
	 * @return true if the {@link #setProgress progress} property has been set.
	 * @see #setProgress
	 */
	public synchronized boolean isProgressPropertyValid() {
		return progressPropertyIsValid;
	}

	/**
	 * Equivalent to {@code getState() == StateValue.STARTED}.
	 * <p>
	 * When a pending Task's state changes to {@code StateValue.STARTED} a
	 * PropertyChangeEvent for the "started" property is fired. Similarly when a
	 * started Task's state changes to {@code StateValue.DONE}, a "done"
	 * PropertyChangeEvent is fired.
	 */
	public final boolean isStarted() {
		return getState() == StateValue.STARTED;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * This method fires the TaskListeners'
	 * {@link TaskServiceEventListener#process process} method. If you override
	 * {@code process} and do not call {@code super.process(values)}, then the
	 * TaskListeners will not run.
	 * 
	 * @param values
	 *            @{inheritDoc}
	 */
	@Override protected void process(List<V> values) {
		fireProcessListeners(values);
	}

	/**
	 * Removes a {@code TaskListener} from this Task. If the specified listener
	 * doesn't exist, this method does nothing.
	 * 
	 * @param listener
	 *            the {@code TaskListener} to be added
	 * @see #addTaskListener
	 */
	@Override public void removeTaskListener(ITaskListener listener) {
		if (listener == null) {
			throw new IllegalArgumentException("null listener");
		}
		taskListeners.remove(listener);
	}

	@Override public void setConnection(NntpConnection conn) {
		this.nntpConnection = conn;
	}

	public void setFailed(boolean failed) {
		boolean oldFailed;
		synchronized (this) {
			oldFailed = this.failed;
			this.failed = failed;
		}
		if (oldFailed != failed) {
			firePropertyChange("message", oldFailed, failed);
		}
	}

	protected void setMessage(String message) {
		String oldMessage, newMessage;
		synchronized (this) {
			oldMessage = this.message;
			this.message = message;
			newMessage = this.message;
			messageTime = System.currentTimeMillis();
		}
		firePropertyChange("message", oldMessage, newMessage);
	}

	@Override public void setPriority(TaskPriority priority) {
		this.priority = priority;
	}

	/**
	 * A convenience method that sets the {@code progress} property to
	 * <code>percentage * 100</code>.
	 * 
	 * @param percentage
	 *            a value in the range 0.0 ... 1.0 inclusive
	 * @see #setProgress(int)
	 */
	protected final void setProgress(float percentage) {
		if (percentage < 0.0 || percentage > 1.0) {
			throw new IllegalArgumentException("invalid percentage");
		}
		setProgress(Math.round(percentage * 100.0f));
	}

	/**
	 * A convenience method that sets the {@code progress} property to the
	 * following ratio normalized to 0 .. 100.
	 * 
	 * <pre>
	 * value - min / max - min
	 * </pre>
	 * 
	 * @param value
	 *            a value in the range min ... max, inclusive
	 * @param min
	 *            the minimum value of the range
	 * @param max
	 *            the maximum value of the range
	 * @see #setProgress(int)
	 */
	protected final void setProgress(float value, float min, float max) {
		if (min >= max) {
			throw new IllegalArgumentException("invalid range: min >= max");
		}
		if (value < min || value > max) {
			throw new IllegalArgumentException("invalid value");
		}
		float percentage = (value - min) / (max - min);
		setProgress(Math.round(percentage * 100.0f));
	}

	/**
	 * A convenience method that sets the {@code progress} property to the
	 * following ratio normalized to 0 .. 100.
	 * 
	 * <pre>
	 * value - min / max - min
	 * </pre>
	 * 
	 * @param value
	 *            a value in the range min ... max, inclusive
	 * @param min
	 *            the minimum value of the range
	 * @param max
	 *            the maximum value of the range
	 * @see #setProgress(int)
	 */
	protected final void setProgress(int value, int min, int max) {
		if (min >= max) {
			throw new IllegalArgumentException("invalid range: min >= max");
		}
		if (value < min || value > max) {
			throw new IllegalArgumentException("invalid value");
		}
		float percentage = (float) (value - min) / (float) (max - min);
		setProgress(Math.round(percentage * 100.0f));
	}

	/**
	 * A convenience method that sets the {@code title} property
	 * 
	 * @param value
	 *            a value in the range min ... max, inclusive
	 * @param title
	 */
	protected void setTitle(String title) {
		String oldValue, newValue;
		synchronized (this) {
			oldValue = this.title;
			this.title = title;
			newValue = this.title;
		}
		firePropertyChange("title", oldValue, newValue);
	}

	/**
	 * Sets the {@code userCanCancel} property. The default value of this
	 * property is true.
	 * <p>
	 * Generic GUI components, like a Task progress dialog, can use this
	 * property to decide if they should provide a way for the user to cancel
	 * this task. For example, the value of this property might be bound to the
	 * enabled property of a cancel button.
	 * <p>
	 * This property has no effect on the {@link #cancel} cancel method. It's
	 * just advice for GUI components that display this Task.
	 * 
	 * @param userCanCancel
	 *            true if the user should be allowed to cancel this Task.
	 * @see #getUserCanCancel
	 */
	protected void setUserCanCancel(boolean userCanCancel) {
		boolean oldValue, newValue;
		synchronized (this) {
			oldValue = this.userCanCancel;
			this.userCanCancel = userCanCancel;
			newValue = this.userCanCancel;
		}
		firePropertyChange("userCanCancel", oldValue, newValue);
	}

	/**
	 * Called when this Task has successfully completed, i.e. when its {@code
	 * get} method returns a value. Tasks that compute a value should override
	 * this method.
	 * <p>
	 * <p>
	 * This method runs on the EDT. It does nothing by default.
	 * 
	 * @param result
	 *            the value returned by the {@code get} method
	 * @see #done
	 * @see #get
	 * @see #failed
	 */
	protected void succeeded(T result) {
		logger.debug("Task: " + getTitle() + " Succeeded");
	}

	private class StatePCL implements PropertyChangeListener {
		public void propertyChange(PropertyChangeEvent e) {
			String propertyName = e.getPropertyName();
			if ("state".equals(propertyName)) {
				StateValue state = (StateValue) e.getNewValue();
				AbstractTask task = (AbstractTask) e.getSource();
				switch (state) {
				case STARTED:
					taskStarted(task);
					break;
				case DONE:
					taskDone(task);
					break;
				}
			}
			else if ("progress".equals(propertyName)) {
				synchronized (AbstractTask.this) {
					progressPropertyIsValid = true;
				}
			}
		}

		private void taskDone(AbstractTask task) {
			synchronized (AbstractTask.this) {
				doneTime = System.currentTimeMillis();
			}
			try {
				task.removePropertyChangeListener(this);
				firePropertyChange("done", false, true);
				fireCompletionListeners();
			}
			finally {
				firePropertyChange("completed", false, true);
			}
		}

		private void taskStarted(AbstractTask task) {
			synchronized (AbstractTask.this) {
				startTime = System.currentTimeMillis();
			}
			firePropertyChange("started", false, true);
			fireDoInBackgroundListeners();
		}
	}
}
